#!/usr/bin/env python
# encoding: utf-8
# vim:fileencoding=utf-8:ts=4:sw=4
#

import os
import re
import types
import json
import traceback
import salt.utils

MASTER_ROOT_DIR = '/vagrant/salt/srv/base/push_guide'
MODULE_NAME = 'push_guide'
LINUX_MINION_ROOT_DIR = '/opt/push_guide/pushed_data'
WINDOWS_MINION_ROOT_DIR = 'C:\push_guide\pushed_data'
LINUX_BACKUP_DIR = '/opt/push_guide/backup'
WINDOWS_BACKUP_DIR = 'C:\push_guide\backup'
G_MINION_ROOT_DIR = ''
G_BACKUP_DIR = ''


if salt.utils.is_windows():
    G_MINION_ROOT_DIR = WINDOWS_MINION_ROOT_DIR
    G_BACKUP_DIR = WINDOWS_BACKUP_DIR
else:
    G_MINION_ROOT_DIR = LINUX_MINION_ROOT_DIR
    G_BACKUP_DIR = LINUX_BACKUP_DIR

def shell_type_to_ext(shell_type):
    if shell_type == 'bash':
        ext = 'sh'
    elif shell_type == 'python':
        ext = 'py'
    elif shell_type == 'ruby':
        ext = 'rb'
    elif shell_type == 'bat':
        ext = 'bat'
    else:
        raise Exception('invalid shell type %s' % shell_type)
    return ext

def ext_to_interpreter(ext):
    if ext == '.sh':
        interpreter = 'bash'
    elif ext == '.py':
        interpreter = 'python'
    elif ext == '.rb':
        interpreter = 'ruby'
    elif ext == '.bat':
        interpreter = 'cmd'
    else:
        raise Exception('invalid file extension %s' % ext)

    if salt.utils.is_windows():
        return interpreter + '.exe'
    else:
        return interpreter

def run_cmd(cmd, error_msg, cwd=None):
    if not cmd:
        raise Exception('cmd is empty')

    result = __salt__['cmd.run_all'](cmd, cwd=cwd)
    if result['retcode'] != 0:
        raise Exception('%s: %s, %s' % (error_msg, cmd, result['stderr']))

def run_shell(shell_path, error_msg):
    if not shell_path:
        raise Exception('shell path is empty')

    if not os.path.exists(shell_path):
        raise Exception('shell file is not exist')

    ext = os.path.splitext(shell_path)[1]
    interpreter = ext_to_interpreter(ext)
    cmd = '%s %s' % (interpreter, shell_path)
    run_cmd(cmd, 'run shell script failed')


def succ_return(msg):
    return {'status': True, 'msg': ''}

def fail_return(msg, exc=''):
    return {'status': False, 'msg': msg, 'exc': exc}



class BaseInstaller(object):

    def __init__(self, **args):
        for k,v in args.items():
            setattr(self, k, v)

    def check_params(self, params):
    # every param should not be empty or None
        for k,v in params.items():
            if not v:
                raise Exception('param %s should not be empty or None' % k)

    def remove_previous(self):
        raise Exception("this method should be implemented in subclass")

    def install(self):
        raise Exception("this method should be implemented in subclass")


class RpmInstaller(BaseInstaller):

    def __init__(self, **args):
        super(RpmInstaller, self).__init__(**args)

    def check_params(self):
        super(RpmInstaller, self).check_params({
            'resource': self.resource,
        })

    def remove_previous(self):
        package_full_name = os.path.basename(self.resource) # nginx-1.6.1-1.el6.ngx.x86_64.rpm
        package_name = package_full_name.split('-')[0] # nginx
        package_prefix = package_full_name.rstrip('.rpm') # nginx-1.6.1-1.el6.ngx.x86_64

        # package does exist and remove it
        run_cmd('rpm -q --quiet %(package_name)s && rpm -e %(package_name)s' % {'package_name': package_name},
               'rpm remove package failed')

    def install(self):
        if self.force_install:
            extra_param = ' --force --nodeps '
        else:
            extra_param = ''
        run_cmd('rpm -U %s %s' % (extra_param, self.resource), 'rpm install failed')


class ArchiveInstaller(BaseInstaller):
    def __init__(self, **args):
        super(ArchiveInstaller, self).__init__(**args)

    def check_params(self):
        super(ArchiveInstaller, self).check_params({
            'resource': self.resource,
            'install_dir': self.install_dir,
        })

    def remove_previous(self):
        try:
            result = __salt__['file.remove'](self.install_dir)
        except Exception as e:
            raise Exception('remove previous files failed %s' % e)

    def extract(self, file_path, install_dir):
        base_name = os.path.basename(file_path) # nginx-1.0.15-11.el6.x86_64.zip
        dir_name = os.path.dirname(file_path)   # /path/to/parent/dir/
        package_name = base_name.split('-')[0]  # nginx

        if file_path.endswith('tar.gz'):
            cmd = 'tar -xzf --overwrite %s' % file_path
        elif file_path.endswith('tar'):
            cmd = 'tar -xf --overwrite %s' % file_path
        elif file_path.endswith('tar.bz2'):
            cmd = 'tar -xjf --overwrite %s' % file_path
        elif file_path.endswith('zip'):
            cmd = 'unzip -o %s' % file_path
        else:
            raise Exception('%s extraction is not suported' % base_name)

        run_cmd(cmd, 'extract package failed', cwd=dir_name)

        try:
            __salt__['file.copy'](os.path.join(dir_name, package_name), self.install_dir, recurse=True)
        except Exception as e:
            raise Exception('move to install dir failed %s' % e)


    def install(self):
        self.extract(self.resource, self.install_dir)


class YumInstaller(BaseInstaller):
    '''
    for YumInstaller, resource is pacakge name, e.g. nginx
    '''
    def __init__(self, **args):
        super(YumInstaller, self).__init__(**args)


    def check_params(self):
        super(YumInstaller, self).check_params({
            'resource': self.resource,
            'version': self.version,
        })

    def remove_previous(self):
        run_cmd('yum remove -y %s' % self.resource, 'yum remove previous package failed')

    def install(self):
        run_cmd('yum install -y %s-%s' % (self.resource, self.version), 'yum install package failed')


class InstallerFactory(object):

    FLAG_RPM = 'rpm'
    FLAG_YUM = 'yum'
    FLAG_ARCHIVE = 'archive'

    FLAG_UPDATE = 'update'
    FLAG_ROLLBACK = 'rollback'

    CLASS_MAP = {
        FLAG_RPM: RpmInstaller,
        FLAG_YUM: YumInstaller,
        FLAG_ARCHIVE: ArchiveInstaller,
    }

    def __init__(self, desc, action):
        self.desc = desc
        self.installer = None
        self.action = action

    def config(self):
        flag = None
        install_action = self.desc.deploy_desc.get('install_action')
        if install_action == self.FLAG_RPM:
            flag = self.FLAG_RPM
        elif install_action == self.FLAG_YUM:
            flag = self.FLAG_YUM
        elif install_action == self.FLAG_ARCHIVE:
            flag = self.FLAG_ARCHIVE
        elif install_action == '':
            # we will guess next step
            pass
        else:
            raise Exception('invalid install_action %s' % install_action)

        # if install_action is not supplied, guess by resource name
        resource = self.desc.global_desc.get('resource_update_to')
        if resource.strip() == '':
            raise Exception('resource can not be empty')

        if resource.endswith('.rpm'):
            flag = self.FLAG_RPM
        elif re.match(r'.*(tar\.gz|tar|tar\.bz2|zip)$', resource):
            flag = self.FLAG_ARCHIVE
        else:
            flag = self.FLAG_YUM

        if flag == self.FLAG_YUM:
            pass
            # yum resource name is 'resource_update_to'
        else:
            # resource is like http://a.com/nginx-1.0.15-11.el6.x86_64.zip
            file_name = resource.split('/')[-1] # nginx-1.0.15-11.el6.x86_64.zip
            resource = os.path.join(G_MINION_ROOT_DIR, self.desc.config.get('task_id'), file_name)

        if self.action == self.FLAG_UPDATE:
            version = self.desc.global_desc.get('version_update_to')
        elif self.action == self.FLAG_ROLLBACK:
            version = self.desc.global_desc.get('version_rollback_to')
        else:
            raise Exception('invalid action %s' % self.action)

        args = {
            'resource': resource,
            'version': version,
            'install_action': install_action,
            'install_dir': self.desc.deploy_desc.get('install_dir'),
            'force_install': self.desc.deploy_desc.get('force_install'),
        }

        installer = self.CLASS_MAP[flag](**args)
        installer.check_params()
        return installer

def rpm_install():
    pass

def archive_install(desc):
    pass

def yum_install():
    pass



class Describer(object):
    def __init__(self, desc):
        self.config = desc
        self.global_desc = self.config['desc']['global_desc']
        self.pre_deploy_desc = self.config['desc']['pre_deploy_desc']
        self.deploy_desc = self.config['desc']['deploy_desc']
        self.post_deploy_desc = self.config['desc']['post_deploy_desc']

    def param_check(self):
        pass



class Deployer(object):
    def __init__(self, job_desc, action):
        self.job_desc = job_desc
        self.action = action
        self.desc = Describer(self.job_desc)


    def validate_install_dir(self):
        protected_dir = ['/', '/usr', '/var', '/boot', '/bin', '/etc']
        protected_dir.extend(['/home', '/lib64', '/root', '/sys', '/dev'])
        protected_dir.extend(['/lib', '/mnt', '/proc', '/sbin', '/tmp'])
        check_slash_depth = 4

        install_dir = self.desc.deploy_desc.get('install_dir', '')

        for i in range(check_slash_depth):
            if install_dir in [s+'/'*i for s in protected_dir]:
                raise Exception('illegal install dir %s' % install_dir)

    def backup(self):
        global G_BACKUP_DIR

        try:
            install_dir = self.desc.deploy_desc.get('install_dir') # eg. /usr/local/demo
            backup_dir = os.path.join(os.path.join(G_BACKUP_DIR, self.desc.config.get('task_id')),
                                     os.path.basename(install_dir))
            if not os.path.exists(backup_dir):
                os.makedirs(backup_dir)

            result = __salt__['file.copy'](
                        install_dir,
                        backup_dir,
                        recurse=True)
        except Exception as e:
            raise Exception('backup failed %s' % e)


    def check_process(self, process):
        if type(process) in [types.ListType, types.TupleType]:
            cmd = 'pidof %s' % ','.join(process)
        else:
            raise Exception('invalid param format of process name')
        run_cmd(cmd, 'check_process failed')


    def check_port(self, port):
        all_alive = True
        if type(port) in [types.ListType, types.TupleType]:
            for p in port:
                cmd = 'ss -ant  src :{port}|grep {port}'.format(port=p)
                run_cmd(cmd, 'check port failed')
        else:
            raise Exception('invalid param format of port')



    def pre_deploy(self):
        inline_shell_type = self.desc.pre_deploy_desc.get('inline_shell_type', '')
        file_path = os.path.join(G_MINION_ROOT_DIR, 
                                 self.desc.config.get('task_id'), 
                                 'pre_deploy_inline.%s' % shell_type_to_ext(inline_shell_type))
        run_shell(file_path, 'run pre_deploy inline shell failed')

    def deploy(self):
        # stop service if needed
        if self.desc.deploy_desc.get('stop_service'):
            run_cmd(self.desc.deploy_desc.get('stop_command'), 'shutdown service failed')

        # validate install dir, system dirs are not allowed to install software
        self.validate_install_dir()

        # backup previous package if needed
        if self.desc.deploy_desc.get('backup_previous_package'):
            self.backup()

        # start to install
        installer = InstallerFactory(self.desc, self.action).config()

        # remove previous package if needed
        if self.desc.deploy_desc.get('remove_previous_package'):
            installer.remove_previous()

        # install
        installer.install()

        # run custom script
        script = self.desc.deploy_desc.get('custom_script')
        if script:
            run_shell(script, 'run script failed at stage deploy')

        # run inline shell
        inline_shell_type = self.desc.deploy_desc.get('inline_shell_type', '')
        file_path = os.path.join(G_MINION_ROOT_DIR, 
                                 self.desc.config.get('task_id'), 
                                 'deploy_inline.%s' % shell_type_to_ext(inline_shell_type))
        run_shell(file_path, 'run deploy inline shell failed')

        # bring up service
        if self.desc.deploy_desc.get('stop_service'):
            if self.desc.deploy_desc.get('start_service'):
                run_cmd(self.desc.deploy_desc.get('start_command'), 'start service failed')
        else:
            if self.desc.deploy_desc.get('start_service'):
                restart_cmd = self.desc.deploy_desc.get('restart_command')
                if restart_cmd:
                    run_cmd(restart_cmd, 'restart service failed')
                else:
                    stop_cmd = self.desc.deploy_desc.get('stop_command')
                    if not (start_cmd and stop_cmd):
                        raise Exception('start_cmd and stop_cmd should not be empty')
                    run_cmd('%(stop)s && %(start)s' % {'stop':stop_cmd, 'start':start_cmd}, 'stop-start service failed')
            else:
                pass



    def post_deploy(self):
        # run custom script
        script = self.desc.post_deploy_desc.get('custom_script')
        if script:
            run_shell(script, 'run script failed at stage post-deploy')

        # run post deploy inline shell
        inline_shell_type = self.desc.post_deploy_desc.get('inline_shell_type', '')
        file_path = os.path.join(G_MINION_ROOT_DIR, 
                                 self.desc.config.get('task_id'), 
                                 'post_deploy_inline.%s' % shell_type_to_ext(inline_shell_type))
        run_shell(file_path, 'run post_deploy inline shell failed')

        # check process alive
        proc_for_check = self.desc.post_deploy_desc.get('check_proc_alive')
        if proc_for_check:
            self.check_process(proc_for_check)


        # check port alive
        port_for_check = self.desc.post_deploy_desc.get('check_port_alive')
        if port_for_check:
            self.check_port(port_for_check)

def get_desc(task_id):
    global G_MINION_ROOT_DIR
    desc_file_path = os.path.join(G_MINION_ROOT_DIR, str(task_id), 'desc.json')
    print task_id, '*'*30
    with open(desc_file_path, 'r') as fp:
        desc = json.load(fp)

    return desc


def deploy(task_id, action='update'):
    try:
        job_desc = get_desc(task_id)
        deployer = Deployer(job_desc, action)
        deployer.pre_deploy()
        deployer.deploy()
        deployer.post_deploy()
        return succ_return('succ')
    except Exception as e:
        return fail_return('deploy failed %s' % e, traceback.format_exc())

if __name__ == '__main__':
    deploy(122, 'update')


